%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "One generic template replaces many near-identical type-specific implementations"
%%| fig-width: 6
%%| fig-height: 3
flowchart TB
Dup["Max(int,int)<br/>Max(float,float)<br/>Max(double,double)<br/>Max(Temperature,Temperature)"]
Tpl["template<typename T><br/>T Max(T a, T b)"]
Dup --> Tpl
W5. C++ Templates, RANGE Metaprogramming, Smart Pointers
1. Summary
1.1 Why Templates? The Problem of Code Duplication
Imagine you need a function that returns the maximum of two integers. Easy:
int Max(int a, int b) {
return a > b ? a : b;
}Now you need the same function for float, double, and a user-defined type Temperature. The straightforward solution is to write a separate function for each type:
float Max(float a, float b) { return a > b ? a : b; }
double Max(double a, double b) { return a > b ? a : b; }
// ... and so on for every typeThis approach has critical problems:
- Hard to maintain: Every bug fix must be applied to all copies. Every test must cover all copies.
- Impossible for libraries: A library author cannot know in advance every type a user might need.
- Scales poorly: In large programs (thousands of types), this becomes unmanageable.
The solution is genericity: write the algorithm once and let the compiler generate type-specific versions automatically. C++ achieves this with templates. Other languages use similar mechanisms under different names:
- Ada, Eiffel: Generics
- Java, C#, Rust, Swift, Scala: Generics
- C++: Templates
1.2 Function Templates
1.2.1 Syntax
A function template is a blueprint for a family of functions. Instead of a concrete type like int, you use a type parameter (a placeholder name, conventionally T):
template <typename T> // Template header: declares T as a type parameter
T Max(T a, T b) // Template body: uses T wherever the type appears
{
return a > b ? a : b;
}
// No semicolon after the closing bracetemplate– keyword that starts a template declarationtypename T– declaresTas a formal type parameter (you may also writeclass T— they are equivalent here)- The template body is identical to what you would write for a concrete type, with
Treplacing the specific type
1.2.2 Template Instantiation: What Happens Behind the Scenes
When you use a function template, the compiler performs template instantiation: it looks at the types of the actual arguments, generates a concrete function (called a function-by-template), and then compiles it.
double x = 3.5, y = 2.1, res;
res = Max(x, y); // Compiler sees: both args are double
// → generates and compiles: double Max_double(double a, double b) { ... }
// → replaces the call with: res = Max_double(x, y);This entire process is automatic — you do not write Max_double yourself. The compiler does it. The generated name Max_double is a conceptual name; the real compiler uses a technique called name mangling.
Key rules of instantiation:
- The actual type is deduced from the types of arguments — not from the return type.
- Each unique combination of types is instantiated only once, even if you call it many times with those types.
- Each different combination of types produces a separate function-by-template:
res = Max(x, y); // Generates Max_double
int k = Max(1, (int)res); // Generates Max_int (a different function!)1.2.3 The Instantiation Algorithm (Step by Step)
When the compiler encounters a call f(actual-arguments):
- If
fis a regular function → compile the call directly. - If
fis a function template:- Determine the type \(T_i\) of each actual argument.
- If the function-by-template for this exact set \(\{T_i\}\) already exists → go to step 3.
- Generate the function-by-template by substituting \(T_i\) for each formal type parameter.
- Compile the generated function.
- Generate code for the function call.
1.2.4 Code Bloat
Template instantiation can cause a problem called code bloat: if two separately compiled files both include the same template header and use the same types, the linker ends up with two identical copies of the same function-by-template in the final executable:
T.h → File1.cpp (includes T.h) → File1.obj (contains Max_double)
→ File2.cpp (includes T.h) → File2.obj (contains Max_double)
→ App.exe (TWO copies of Max_double!)
Modern linkers can merge duplicates, and the C++ Standard provides mechanisms (explicit instantiation, extern template) to control this, but it is a real concern in large projects.
1.2.5 Requirements on Actual Types
A template does not work with any type — it works only with types that support the operations used inside the template body. For Max, the body contains a > b, so the actual type must have operator>.
class C {
int m;
public:
C() : m(0) { }
// No operator> defined!
};
C c1, c2;
Max(c1, c2); // Compile error: 'binary >': 'class C' doesn't define this operatorThe compiler error message points directly to the generated Max_C function.
Solution: Add the required operator to the class:
class C {
int m;
public:
C() : m(0) { }
bool operator>(const C& c) const { return m > c.m; }
};
// Now Max(c1, c2) works correctlyThe general principle: A function template implicitly requires that every actual type supports all operations used inside the template body. Violation is a compile-time error.
1.2.6 C++20 Concepts: Formal Type Requirements
Before C++20, these requirements were informal (written as comments) and only caught at instantiation time — often with confusing error messages. Concepts allow you to formally specify requirements:
template<typename T>
concept GreaterThan =
requires(T x, T y) { { x > y } -> std::same_as<bool>; };
template<typename T> requires GreaterThan<T>
T Max(T a, T b) {
return a > b ? a : b;
}Now if you try to use Max with a type that lacks operator>, the compiler reports a clear error mentioning the violated concept, not a cryptic internal error inside the template body.
1.2.7 Explicit Instantiation of Function Templates
Sometimes the compiler cannot deduce the template type from the arguments — for example, when a function has no arguments:
template <typename T>
int spaceOf() {
int bytes = sizeof(T);
return bytes / 4 + (bytes % 4 > 0 ? 1 : 0);
}
// int w = spaceOf(); // ERROR: compiler cannot deduce TThe solution is explicit instantiation — you specify the type in angle brackets at the call site:
int wint = spaceOf<int>(); // Explicitly: T = int
int wdouble = spaceOf<double>(); // Explicitly: T = doubleThe compiler instantiates the template with the given type, then optionally applies optimization (e.g., sizeof(int) is a compile-time constant, so the whole function may be inlined to a constant):
spaceOf<int>() → generates spaceOf_int() → inlines to constant 1
int wint = spaceOf<int>(); → int wint = 1;
1.3 Class Templates
A class template is a blueprint for a family of classes — just as a function template is a blueprint for a family of functions.
- A class is a type.
- A class template is not a type; it is a family of types.
1.3.1 Motivating Example: A Stack
A stack (also called LIFO — Last In, First Out) is a data structure with three fundamental operations:
- push: Insert an element on top.
- pop: Remove and return the top element.
- isEmpty: Check whether the stack is empty.
A non-template C++ implementation for integers looks like this:
class Stack {
int top;
int S[100]; // Array of integers
public:
Stack() : top(-1) { }
void push(int V) { S[++top] = V; }
int pop() { return S[top--]; }
bool isEmpty() { return top < 0; }
};To create a stack of double values you would need to copy the entire class and replace every int with double. This is the same code-duplication problem as with function templates. The template solution:
template <typename T> // T is the element type
class Stack {
int top;
T S[100]; // Array of T
public:
Stack() : top(-1) { }
void push(T V) { S[++top] = V; }
T pop() { return S[top--]; }
bool isEmpty() { return top < 0; }
};Now T stands for any type — integer, double, string, or user-defined.
1.3.2 Class Template Instantiation
Unlike function templates (which are instantiated implicitly from argument types), class templates must be instantiated explicitly using the <actual-type> syntax:
Stack<int> sint; // Stack of integers
Stack<double> sdouble; // Stack of doubles
Stack<string> sstr; // Stack of strings
Stack<float> sf1, sf2; // Two stacks of floats
Stack<int>* arrayOfStacks[10]; // Array of 10 pointers to int-stacksThe notation Stack<int> is a type specifier — it names a class generated by the compiler from the Stack template by substituting int for T. This generated class behaves exactly like an ordinary class.
Using a class-by-template:
Stack<int> s;
s.push(1);
s.push(2);
int v = s.pop(); // v = 2Type aliases (two equivalent syntaxes):
typedef Stack<double> SD; // C++98/03
using SD = Stack<double>; // C++11 (preferred)
SD sd1, sd2;1.3.3 Templates, Classes, and Instances — Three Levels
It is important to distinguish three conceptually different entities:
| Level | Entity | Created by | Exists at |
|---|---|---|---|
| Template | Stack<T> (blueprint) |
Programmer | Source code |
| Class-by-template | Stack<int>, Stack<double> |
Compiler (instantiation) | Compile time |
| Object (instance) | Stack<int> s; |
Runtime (new or stack allocation) |
Runtime |
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "From template definition to class template specialization to runtime object"
%%| fig-width: 6.3
%%| fig-height: 3
flowchart LR
Def["template<typename T><br/>class Stack"]
Spec1["Stack<int>"]
Spec2["Stack<double>"]
Obj["Stack<int> s"]
Def --> Spec1
Def --> Spec2
Spec1 --> Obj
1.3.4 Requirements on Actual Types for Class Templates
Inside Stack, the push method does:
void push(T V) { S[++top] = V; }Two operations on type T are used:
- Passing
Vby value (T V) — invokes the copy constructor. - Assigning
= V— invokes the assignment operator.
Therefore, to use Stack<MyClass>, MyClass must have a public copy constructor and a public assignment operator. The compiler auto-generates both for most classes, but there are exceptions (e.g., if copying is expensive or explicitly deleted). To avoid invoking the copy constructor on push, pass by reference:
void push(const T& V) { S[++top] = V; } // No copy constructor needed for the callSimilarly, pop returns by value, which also copies. A common improvement is to return a reference:
T& pop() { return S[top--]; }1.3.5 Non-Type Template Parameters
Template parameters do not have to be types — they can also be constant values (integers, pointers, etc.). This allows parameterizing the behavior or dimensions of a class at compile time, not just its element type.
The fixed-size array T S[100] inside Stack is a design limitation — every stack has exactly 100 slots regardless of need. We can make the size a template parameter:
template <typename T, int N> // Two parameters: type T and integer N
class Stack {
int top;
T S[N]; // Size is now N, determined at compile time
public:
Stack() : top(-1) { }
void push(const T& V) { S[++top] = V; }
T pop() { top--; return S[top + 1]; }
bool isEmpty() { return top < 0; }
};Usage:
Stack<int, 10> s10; // Stack of 10 integers
Stack<double, 50> s50; // Stack of 50 doublesImportant constraint: Non-type template arguments must be compile-time constants. You cannot pass a runtime variable as a non-type template argument.
Allowed non-type arguments:
- Constant integral expressions (e.g.,
10,sizeof(int) * 4) - Names of objects with external linkage
- Addresses of objects/functions with external linkage
1.3.6 Multi-Parameter Class Templates
Templates can have multiple type parameters. A Dictionary (map from keys to values) naturally needs two types:
#include <map>
#include <string>
template<typename K, typename V>
class Dictionary {
std::map<K, V> data;
public:
void insert(const K& key, const V& value) {
data[key] = value;
}
V get(const K& key) const {
auto it = data.find(key);
if (it != data.end()) return it->second;
throw std::runtime_error("Key not found");
}
bool contains(const K& key) const {
return data.find(key) != data.end();
}
void remove(const K& key) {
data.erase(key);
}
};
// Usage:
Dictionary<int, std::string> myDict;
myDict.insert(1, "One");
myDict.insert(2, "Two");
std::cout << myDict.get(1) << std::endl; // "One"1.4 Metaprogramming Case Study: RANGE Types
Metaprogramming means using the language’s type system and templates to enforce constraints at compile time rather than at runtime. The RANGE example shows this beautifully.
1.4.1 The Problem with Primitive Integer Types
Consider a variable currentDay that should hold a day of the month (1–31). Using int:
int currentDay, currentMonth;
currentDay = 70; // Nonsense — but the compiler accepts it!
currentDay = currentMonth + 1; // Mixing day and month? The compiler won't warn.Languages like Pascal and Ada solve this with range types:
type DayOfMonth = 1..31; // Pascaltype DayOfMonth is Integer range 1..31; -- AdaC++ has no built-in range types. However, using classes and templates, we can build one.
1.4.2 First Attempt: A Naive RANGE Class
The idea: create a class that stores a value together with its allowed boundaries, and checks the value on every modification.
class RANGE {
int leftBorder;
int rightBorder;
int value;
public:
// Constructor: set borders and initial value
RANGE(int v, int l, int r) {
leftBorder = l;
rightBorder = r;
value = v;
check(); // Verify immediately
}
// Deleted default constructor: forbid uninitialized RANGE objects
RANGE() = delete;
// Copy constructor
RANGE(const RANGE& r) {
leftBorder = r.leftBorder;
rightBorder = r.rightBorder;
value = r.value;
}
// Assignment from another RANGE
RANGE& operator=(RANGE& r) { value = r.value; return *this; }
// Assignment from int
RANGE& operator=(int v) { value = v; check(); return *this; }
// Increment
RANGE& operator++() { value++; check(); return *this; }
// Conversion to int (so RANGE can be used where int is expected)
operator int() { return value; }
private:
void check() {
if (value < leftBorder || value > rightBorder)
throw std::out_of_range("RANGE value out of bounds");
}
};Why does operator= return *this? To support chained assignments: a = b = c requires the assignment to return a reference to the left operand.
Usage:
RANGE range(0, -10, 10); // value=0, allowed: [-10, 10]
range = 5; // OK
range = 15; // throws exception
++range; // increments, checks
int i = range; // uses operator int()1.4.3 Problems with the Naive Approach
This works but has two fundamental weaknesses:
Problem 1 — Boundaries are value attributes, not type attributes:
RANGE a(0, -5, 5);
RANGE b(3, 1, 10);
a = b; // Compiles! But semantically wrong — different ranges!a and b are variables of the same type (RANGE), even though they represent completely different value domains. Ideally, RANGE(-5,5) and RANGE(1,10) should be different types so that assigning one to the other is a compile-time error.
Problem 2 — Storage overhead:
Each RANGE object carries three integers: value, leftBorder, rightBorder. The borders never change after construction — they are logically part of the type, not the value. There is no reason to store them repeatedly in every object.
1.4.4 Template Solution for RANGE
The fix is to make the borders template parameters (compile-time constants) instead of runtime data members. Then each (leftBorder, rightBorder) pair creates a distinct type:
template <int leftBorder, int rightBorder>
class RANGE {
int value; // Only member: the stored value
RANGE() = delete; // No default constructor
public:
RANGE(int v) { value = v; check(); }
RANGE(const RANGE& r) { value = r.value; }
RANGE& operator=(RANGE& r) { value = r.value; return *this; }
RANGE& operator=(int v) { value = v; check(); return *this; }
RANGE& operator++() { value++; check(); return *this; }
operator long() { return (long)value; }
private:
void check() {
if (value < leftBorder || value > rightBorder)
throw std::out_of_range("RANGE value out of bounds");
}
};Now:
RANGE<-5, 5> a(0); // Type: RANGE<-5,5>, stores only one int
RANGE<1, 10> b(3); // Type: RANGE<1,10>, completely different type
a = b; // COMPILE ERROR: different types — assignment operator is type-safe!RANGE<-5,5> and RANGE<1,10> are different classes generated from the same template. Each stores only one integer (the value); the borders are baked into the type name at compile time — zero runtime overhead.
Type aliases make usage convenient:
typedef RANGE<-5, 5> myTinyInt; // C++98
using myTinyInt = RANGE<-5, 5>; // C++11 (preferred)
myTinyInt i = 2; // Clean, type-safe syntaxThe family of types RANGE<-5,5>, RANGE<1,10>, RANGE<100,1000> etc. are all distinct types generated from the same template. They are incompatible with each other, just like int and double are incompatible.
1.5 Problems with Raw C/C++ Pointers
Before learning how to fix pointer problems, it is essential to understand what those problems are. Scott Meyers identified six categories of problems with raw pointers (plus additional ones).
1.5.1 Ambiguity: Single Object vs Array (Problems 1 & 4)
A pointer T* ptr can point to either a single object or the first element of an array — there is no way to tell from the pointer type alone:
int x;
int A1[10];
int* A2 = &x;
int* A = cond ? A1 : A2;
int res = A[5]; // Is this valid? Depends on cond — undefined behavior!Consequently, there are two different deallocation operators — delete ptr (for single objects) and delete[] ptr (for arrays) — and using the wrong one is undefined behavior.
1.5.2 Ownership Ambiguity (Problem 2)
Looking at a pointer declaration, you cannot tell whether the function “owns” the object (i.e., is responsible for destroying it):
void fun(T* ptr) {
// Do some work with *ptr
// Should we destroy it? We don't know!
return;
}This ambiguity leads to memory leaks (if nobody deletes) or double-deletion (if multiple callers each try to delete).
1.5.3 Destruction Method Ambiguity (Problem 3)
Even if you know you must destroy the object, you may not know how:
void fun(T* ptr) {
// Work...
free(ptr); // Correct? Or should it be:
// myDealloc(ptr); // a custom deallocator?
// delete ptr; // Or this?
}Different allocation schemes require different deallocation functions.
1.5.4 Double Destruction (Problem 5)
When multiple code paths share a pointer, it is easy to destroy the object twice:
void lib_fun(T* ptr) {
// Does lib_fun delete the object? Hard to know without reading all source code.
}
void user_fun() {
T* ptr = new T();
lib_fun(ptr);
delete ptr; // Is this a double-delete if lib_fun already deleted it?
}Double-deletion is undefined behavior — it can corrupt the heap and cause security vulnerabilities.
1.5.5 Dangling Pointers (Problem 6)
There is no built-in way to check whether a pointer still points to a valid, live object:
T* ptr = new T();
if (condition) delete ptr;
// ...
// Long code later...
// Is ptr still valid? Cannot know without tracking control flow manually.A dangling pointer is a pointer that refers to memory that has already been freed. Dereferencing it is undefined behavior.
Classic dangling pointer with stack allocation:
int* p;
void f() {
int A[10];
p = A + 2; // p points into f's stack frame
} // A goes out of scope — memory is freed
int main() {
f();
*p = 777; // p is now dangling — undefined behavior!
}When f returns, its entire stack frame (including A) is gone. p still holds an address into that now-invalid memory.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Dangling pointer: pointer survives after the stack object it points to has been destroyed"
%%| fig-width: 6.2
%%| fig-height: 3.2
flowchart TB
subgraph Frame["Function f stack frame"]
A["A : int = 42"]
P["p = &A"]
end
After["f returns<br/>stack frame removed"]
Dangling["p still stores old address<br/>dangling pointer"]
Frame --> After --> Dangling
style Frame fill:#f9fbfd,stroke:#355c7d,color:#1f2d3d
style A fill:#d6eef5,stroke:#355c7d,color:#1f2d3d
style P fill:#e8f4f8,stroke:#355c7d,color:#1f2d3d
style After fill:#eef3f7,stroke:#355c7d,color:#1f2d3d
style Dangling fill:#f9d9e2,stroke:#355c7d,color:#1f2d3d
1.5.6 Memory Leaks (Problem 7)
If the last (or only) pointer to a dynamically allocated object goes out of scope without delete being called, the object persists in memory but is forever unreachable — a memory leak:
void f() {
int* p = new int(42); // Dynamic object allocated on heap
// ... (no delete)
} // p goes out of scope; the int(42) still lives on the heap but cannot be reached!In a long-running application, accumulated leaks can exhaust all available memory.
%%{init: {'theme': 'base', 'themeVariables': { 'fontFamily': 'Helvetica', 'primaryColor': '#e8f4f8', 'primaryTextColor': '#1f2d3d', 'primaryBorderColor': '#355c7d', 'lineColor': '#355c7d', 'secondaryColor': '#d6eef5', 'tertiaryColor': '#fff3cd', 'background': '#ffffff', 'mainBkg': '#e8f4f8', 'secondBkg': '#d6eef5', 'tertiaryBkg': '#fff3cd', 'clusterBkg': '#f9fbfd', 'clusterBorder': '#355c7d', 'edgeLabelBackground': '#ffffff' }}}%%
%%| fig-cap: "Memory leak: the heap object remains allocated after the last owning pointer disappears"
%%| fig-width: 6
%%| fig-height: 3
flowchart LR
P["p : int*"]
H["heap object<br/>new int(42)"]
Gone["p goes out of scope"]
Leak["unreachable heap object<br/>memory leak"]
P --> H
H --> Gone --> Leak
1.6 Smart Pointers
Smart pointers are class templates that wrap raw pointers and provide automatic resource management. They add “smart” behavior — like automatic deletion — without losing the efficiency of raw pointers. All require #include <memory>.
The key design pattern behind smart pointers is RAII — Resource Acquisition Is Initialization: resources are acquired in the constructor and released in the destructor. Since destructors are called automatically when objects go out of scope, resource cleanup is guaranteed.
1.6.1 General Idea: A Simple Smart Pointer
A minimal smart pointer wraps a raw pointer and deletes it automatically when it goes out of scope:
template <typename T>
class smart_pointer {
T* obj; // The underlying raw pointer
public:
smart_pointer(T* o) : obj(o) { } // Takes ownership
~smart_pointer() { delete obj; } // Guaranteed cleanup
T* operator->() { return obj; } // Arrow dereference
T& operator*() { return *obj; } // Star dereference
};{
smart_pointer<MyClass> sp(new MyClass());
sp->someMethod(); // Works like a raw pointer
} // Destructor called — MyClass object is deleted automaticallyNo delete required from the caller. C++ provides three production-quality smart pointer templates.
1.6.2 unique_ptr — Exclusive Ownership
unique_ptr implements exclusive ownership: at any given moment, exactly one unique_ptr owns the object. This is enforced by the compiler: unique_ptr cannot be copied.
#include <memory>
std::unique_ptr<int> x(new int(42));
std::unique_ptr<int> y;
y = x; // COMPILE ERROR: copy is forbidden
std::unique_ptr<int> z(x); // COMPILE ERROR: copy constructor is deletedTransferring ownership is possible with std::move, which moves ownership from one pointer to another and nullifies the source:
y = std::move(x); // y now owns the object; x becomes nullptrC++14 factory function (make_unique) — preferred over new because it is exception-safe:
auto x = std::make_unique<int>(42); // No explicit newKey member functions:
x.get()— returns the underlying raw pointer (without releasing ownership)x.reset()— destroys the owned object and sets the pointer tonullptrx.release()— returns the raw pointer and relinquishes ownership (caller is now responsible)
unique_ptr solves:
- Memory leaks (automatic deletion)
- Double deletion (only one owner)
- Ownership clarity (intent is explicit in the type)
1.6.5 weak_ptr — Breaking Circular References
weak_ptr is a non-owning observer of an object managed by shared_ptr. It does not increment the reference count, so it does not prevent the object from being deleted. However, it can check whether the object still exists and temporarily promote itself to a shared_ptr for safe access.
auto shared = std::make_shared<MyObj>(); // ref count = 1
std::weak_ptr<MyObj> weak(shared); // ref count still = 1 (weak does not count)
shared = nullptr; // ref count drops to 0 — object deleted
// weak now points to an expired (destroyed) object
if (weak.expired()) {
// Object is gone
}
auto temp = weak.lock(); // Attempts to get a shared_ptr
if (temp) {
// Object still exists — use temp safely
}Solving circular references with weak_ptr: Break one link in the cycle by making it a weak_ptr:
class Bar;
class Foo {
public:
std::shared_ptr<Bar> bar; // Foo owns Bar (strong reference)
};
class Bar {
public:
std::weak_ptr<Foo> foo; // Bar observes Foo but does NOT own it (weak reference)
};
void fun() {
auto foo = std::make_shared<Foo>(); // foo ref count = 1
foo->bar = std::make_shared<Bar>(); // bar ref count = 1
foo->bar->foo = foo; // foo ref count stays 1 (weak_ptr!)
}
// fun exits: foo ref count = 1 → 0 → Foo deleted → Bar's shared_ptr lost → bar ref count = 1 → 0 → Bar deleted
// No leak!Summary of smart pointer types:
| Type | Ownership | Copy | ref count | Use case |
|---|---|---|---|---|
unique_ptr |
Exclusive | No (move only) | No | Single owner, no sharing |
shared_ptr |
Shared | Yes | Yes (ARC) | Multiple owners |
weak_ptr |
None (observer) | Yes | No | Break cycles, optional references |
2. Definitions
- Template: A C++ language feature that allows writing generic code parameterized by types or compile-time values, so the compiler can generate concrete functions or classes for any given type.
- Function template: A template that acts as a blueprint for a family of functions; the compiler generates a concrete function (function-by-template) each time the template is used with a specific type.
- Class template: A template that acts as a blueprint for a family of classes; must be explicitly instantiated with
TemplateName<ActualType>before use. - Template parameter: A placeholder in a template definition — either a type parameter (declared with
typename Torclass T) or a non-type parameter (a compile-time constant, e.g.,int N). - Template instantiation: The process by which the compiler generates a concrete function or class from a template by substituting actual types (or values) for formal type parameters.
- Function-by-template: A concrete function generated by instantiating a function template with a specific type (e.g.,
Max_doublefromMax<T>). - Class-by-template: A concrete class generated by instantiating a class template with a specific type (e.g.,
Stack<int>fromStack<T>). - Implicit instantiation: Template instantiation triggered automatically by the compiler when a function template is called with typed arguments.
- Explicit instantiation: Template instantiation where the programmer specifies the type explicitly using angle-bracket syntax (e.g.,
spaceOf<int>()), needed when the compiler cannot deduce the type from arguments. - Non-type template parameter: A template parameter that is a compile-time constant value (typically an integer), rather than a type; e.g.,
template <typename T, int N>. - Code bloat: The problem where template instantiation produces multiple copies of the same function-by-template in separate object files, leading to a larger-than-necessary executable.
- Type requirements: The set of operations a type must support to be used as an actual type parameter in a template; violations produce compile-time errors.
- Concept (C++20): A formal, compiler-checkable specification of requirements on template type parameters, enabling clearer error messages when requirements are violated.
- Genericity: The ability to write code that works with any type satisfying certain requirements, rather than being tied to specific concrete types.
- RANGE type: A user-defined type that restricts integer values to a specified compile-time interval; implemented via a class template with non-type parameters for the boundaries.
- Metaprogramming: A programming technique where the program manipulates code (types, values, structures) at compile time using the template system, rather than at runtime.
- Raw pointer: A plain C/C++ pointer (
T*) with no built-in ownership semantics or automatic cleanup. - Dangling pointer: A pointer that refers to memory that has already been freed or gone out of scope; dereferencing it is undefined behavior.
- Memory leak: The situation where a dynamically allocated object is never freed because the last pointer to it has gone out of scope; the memory remains allocated but inaccessible.
- RAII (Resource Acquisition Is Initialization): A C++ design pattern where a resource (memory, file handle, mutex, etc.) is acquired in a constructor and released in the destructor, guaranteeing cleanup when the object goes out of scope.
- Smart pointer: A class template that wraps a raw pointer and adds automatic resource management (and possibly ownership tracking) via RAII.
unique_ptr: A smart pointer implementing exclusive ownership — only oneunique_ptrmay own an object at a time; the object is deleted when theunique_ptris destroyed. Cannot be copied; ownership transferred viastd::move.shared_ptr: A smart pointer implementing shared ownership via Automatic Reference Counting (ARC); the object is deleted when the lastshared_ptrpointing to it is destroyed.weak_ptr: A non-owning observer that references an object managed byshared_ptrwithout incrementing its reference count; used to break circular references. Access requires converting toshared_ptrvialock().- Automatic Reference Counting (ARC): A memory management strategy where an integer counter tracks the number of owning references to an object; the object is destroyed when the count reaches zero.
- Reference count: The integer maintained by
shared_ptrrepresenting how manyshared_ptrinstances currently own a given object. - Circular reference: A situation where two or more objects hold
shared_ptrpointers to each other, forming a cycle; their reference counts never reach zero, causing a memory leak. std::move: A cast to an rvalue reference; used to transfer ownership of aunique_ptr(or other move-only type) without copying.make_unique<T>(...)(C++14): Factory function that creates an object and wraps it in aunique_ptrin a single exception-safe step.make_shared<T>(...): Factory function that creates an object and its reference-count control block together, wrapping the result in ashared_ptr.weak_ptr::expired(): Returnstrueif the object theweak_ptrwas observing has already been destroyed (i.e., reference count reached zero).weak_ptr::lock(): Attempts to obtain ashared_ptrto the observed object; returns an emptyshared_ptrif the object no longer exists.- Control block: An internal data structure allocated alongside the managed object in
shared_ptr; stores the reference count and weak reference count.
3. Examples
3.1. Generic Stack with Specialized StringStack (Lab 5, Task 1)
Implement a generic GenericStack<T> class template that supports push(), pop(), and peek(). The stack should resize dynamically. Then create a StringStack subclass that:
- Rejects empty strings on
push() - Adds a
concatTopTwo()method that pops the top two strings, concatenates them, and pushes the result
Include error handling for edge cases.
Click to see the solution
Key Concept: Templates and inheritance work together — StringStack inherits from GenericStack<string>, gaining all generic functionality and customizing or extending it for strings.
#include <iostream>
#include <vector>
#include <stdexcept>
#include <string>
using namespace std;
// -------- GenericStack<T> --------
template <typename T>
class GenericStack {
protected:
vector<T> data; // vector handles dynamic resizing automatically
public:
// Constructor: optional initial capacity (vector still grows as needed)
explicit GenericStack(int initialCapacity = 0) {
data.reserve(initialCapacity);
}
virtual ~GenericStack() = default;
// Insert element on top
virtual void push(const T& element) {
data.push_back(element);
}
// Remove and return top element
virtual T pop() {
if (isEmpty()) throw underflow_error("pop() on empty stack");
T top = data.back();
data.pop_back();
return top;
}
// Return top element without removing it
virtual T peek() const {
if (isEmpty()) throw underflow_error("peek() on empty stack");
return data.back();
}
bool isEmpty() const { return data.empty(); }
int size() const { return (int)data.size(); }
};
// -------- StringStack --------
class StringStack : public GenericStack<string> {
public:
explicit StringStack(int capacity = 0) : GenericStack<string>(capacity) { }
// Override push: reject empty strings
void push(const string& s) override {
if (s.empty())
throw invalid_argument("StringStack::push: empty string not allowed");
GenericStack<string>::push(s); // Delegate to base
}
// New method: pop top two strings, concatenate, push result
void concatTopTwo() {
if (size() < 2)
throw underflow_error("concatTopTwo: need at least 2 elements");
string second = pop(); // top
string first = pop(); // second from top
push(first + second); // push concatenation
}
};
int main() {
cout << "=== GenericStack<int> ===" << endl;
GenericStack<int> intStack(5);
intStack.push(10);
intStack.push(20);
intStack.push(30);
cout << "Peek: " << intStack.peek() << endl; // 30
cout << "Pop: " << intStack.pop() << endl; // 30
cout << "Pop: " << intStack.pop() << endl; // 20
cout << "\n=== StringStack ===" << endl;
StringStack ss;
ss.push("Hello, ");
ss.push("World!");
cout << "Top before concat: " << ss.peek() << endl; // "World!"
ss.concatTopTwo();
cout << "After concatTopTwo: " << ss.peek() << endl; // "Hello, World!"
// Try pushing empty string
try {
ss.push("");
} catch (const invalid_argument& e) {
cout << "Error: " << e.what() << endl;
}
// Try concatTopTwo on single element
try {
ss.concatTopTwo();
} catch (const underflow_error& e) {
cout << "Error: " << e.what() << endl;
}
return 0;
}Output:
=== GenericStack<int> ===
Peek: 30
Pop: 30
Pop: 20
=== StringStack ===
Top before concat: World!
After concatTopTwo: Hello, World!
Error: StringStack::push: empty string not allowed
Error: concatTopTwo: need at least 2 elements
- Template base:
GenericStack<T>works with any type;vector<T>handles dynamic sizing automatically. - Inheritance:
StringStack : public GenericStack<string>inherits all generic operations. - Override
push: Adds validation before delegating to the base implementation viaGenericStack<string>::push(s). concatTopTwo: Pops in LIFO order — the first popped is the top (second word), the second popped is below (first word). They must be concatenated in the correct order.- Virtual destructor in base:
virtual ~GenericStack() = defaultensures correct destruction through base pointers.
Answer: See full implementation above. Inheritance combined with templates allows clean specialization of generic containers.
3.2. Smart Pointers in Practice: Box Class (Lab 5, Task 2)
Create a class Box with an integer value, a constructor, and a destructor (with output). Then implement and demonstrate:
(a) create_unique(int val) — creates a Box via unique_ptr, demonstrates ownership transfer, and returns the value inside the Box.
(b) create_shared_boxes() — creates two shared_ptr<Box> instances and demonstrates how the reference count changes.
(c) A weak_ptr<Box> example — show checking validity and converting weak_ptr to shared_ptr for access. Explain how weak_ptr solves circular references.
Click to see the solution
Key Concept: Each smart pointer type has a distinct ownership model. unique_ptr → single owner, shared_ptr → shared ownership with ARC, weak_ptr → non-owning observer.
#include <iostream>
#include <memory>
using namespace std;
// ---- Box class ----
class Box {
public:
int value;
Box(int v) : value(v) {
cout << "Box(" << value << ") created\n";
}
~Box() {
cout << "Box(" << value << ") destroyed\n";
}
};
// ---- (a) unique_ptr: exclusive ownership ----
int create_unique(int val) {
auto box = make_unique<Box>(val); // Box created, ref count = 1 (conceptually)
cout << "box value: " << box->value << endl;
// Transfer ownership: box2 takes over, box becomes null
auto box2 = move(box);
cout << "After move: box is " << (box ? "valid" : "null") << endl;
cout << "box2 value: " << box2->value << endl;
int result = box2->value;
return result;
// box2 goes out of scope → Box destroyed automatically
}
// ---- (b) shared_ptr: cooperative ownership ----
void create_shared_boxes() {
auto boxA = make_shared<Box>(10); // ref count = 1
cout << "boxA ref count: " << boxA.use_count() << endl; // 1
auto boxB = make_shared<Box>(20); // ref count = 1
cout << "boxB ref count: " << boxB.use_count() << endl; // 1
{
auto boxA2 = boxA; // ref count = 2 (shared ownership)
cout << "After boxA2 = boxA, ref count: " << boxA.use_count() << endl; // 2
auto boxA3 = boxA; // ref count = 3
cout << "After boxA3 = boxA, ref count: " << boxA.use_count() << endl; // 3
}
// boxA2 and boxA3 go out of scope → ref count = 1, object NOT deleted
cout << "After scope: boxA ref count: " << boxA.use_count() << endl; // 1
}
// boxA goes out of scope → ref count = 0 → Box(10) destroyed
// ---- (c) weak_ptr: non-owning reference ----
void demonstrate_weak() {
auto shared = make_shared<Box>(42); // ref count = 1
weak_ptr<Box> weak(shared); // ref count still = 1
cout << "shared ref count: " << shared.use_count() << endl; // 1
cout << "weak expired? " << weak.expired() << endl; // 0 (false)
// Safe access via lock()
if (auto temp = weak.lock()) {
cout << "Accessed via weak: " << temp->value << endl; // 42
}
shared.reset(); // Explicitly destroy the shared_ptr → ref count = 0 → Box destroyed
cout << "After reset, weak expired? " << weak.expired() << endl; // 1 (true)
if (!weak.lock()) {
cout << "Object is gone — weak_ptr is safe to use (returns null)\n";
}
}
int main() {
cout << "=== (a) unique_ptr ===" << endl;
int v = create_unique(100);
cout << "Returned value: " << v << "\n" << endl;
cout << "=== (b) shared_ptr ===" << endl;
create_shared_boxes();
cout << endl;
cout << "=== (c) weak_ptr ===" << endl;
demonstrate_weak();
return 0;
}Output:
=== (a) unique_ptr ===
Box(100) created
box value: 100
After move: box is null
box2 value: 100
Box(100) destroyed
Returned value: 100
=== (b) shared_ptr ===
Box(10) created
boxA ref count: 1
Box(20) created
boxB ref count: 1
After boxA2 = boxA, ref count: 2
After boxA3 = boxA, ref count: 3
After scope: boxA ref count: 1
Box(10) destroyed
Box(20) destroyed
=== (c) weak_ptr ===
shared ref count: 1
weak expired? 0
Accessed via weak: 42
Box(42) destroyed
After reset, weak expired? 1
Object is gone — weak_ptr is safe to use (returns null)
unique_ptr(a):move(box)transfers ownership and nullifiesbox. The object is destroyed whenbox2leavescreate_unique’s scope.shared_ptr(b): Each copy of ashared_ptr(assignment or copy construction) incrementsuse_count(). When all copies go out of scope,use_count()reaches 0 and the object is destroyed.weak_ptr(c):weak_ptr<Box> weak(shared)creates an observer without incrementing the ref count.shared.reset()drops the ref count to 0 → Box is destroyed.weak.expired()returnstrueafter destruction.weak.lock()returns an emptyshared_ptrsafely — no undefined behavior.
- Circular reference solution: If
Boxcontained ashared_ptr<Box>pointing to another Box that pointed back, neither would be deleted. Making one of those linksweak_ptr<Box>breaks the cycle.
Answer: See complete implementation above demonstrating all three smart pointer types and their lifetime semantics.
3.3. Max Function Template: Find the Maximum of Two Values (Lecture 5, Example 1)
Write a function template Max that returns the maximum of two values of any type that supports the > operator. Demonstrate its use with int, double, and a user-defined class.
Click to see the solution
Key Concept: A function template is written once with a type placeholder T. The compiler generates concrete versions (Max_int, Max_double, etc.) on demand based on the argument types at each call site.
#include <iostream>
using namespace std;
// Function template: works for any type T that has operator>
template <typename T>
T Max(T a, T b) {
return a > b ? a : b;
}
// A user-defined class that satisfies the template requirement (has operator>)
class Temperature {
double celsius;
public:
Temperature(double c) : celsius(c) { }
bool operator>(const Temperature& t) const { return celsius > t.celsius; }
double value() const { return celsius; }
};
int main() {
// Implicit instantiation for int (compiler generates Max_int)
cout << Max(3, 7) << endl; // 7
// Implicit instantiation for double (compiler generates Max_double)
cout << Max(3.14, 2.71) << endl; // 3.14
// Implicit instantiation for Temperature (compiler generates Max_Temperature)
Temperature t1(36.6), t2(38.2);
cout << Max(t1, t2).value() << endl; // 38.2
return 0;
}- Template declaration:
template <typename T>introduces type parameterT. The bodyreturn a > b ? a : b;usesTwherever a type is needed. - Implicit instantiation: When the compiler sees
Max(3, 7), it deducesT = intfrom the argument types and generatesint Max_int(int a, int b)internally. - Type requirement:
Temperaturemust haveoperator>— without it, the compiler reports an error inside the generatedMax_Temperaturefunction. - Separate instantiations:
Max_int,Max_double, andMax_Temperatureare three distinct functions in the compiled output.
Answer: The template generates three functions from one definition. Output: 7, 3.14, 38.2.
3.4. alignArray: Converting a Specific Function to a Template (Lecture 5, Example 2)
Given the following integer-specific function, convert it into a function template that works with arrays of any numeric type. Declare a class that satisfies the template’s requirements, then demonstrate the template with an array of that class.
void alignArray(int* array, int size, int barrier) {
for (int i = 0; i < size; i++) {
if (array[i] < barrier) array[i] += 2;
else if (array[i] > barrier) array[i] -= 2;
}
}Click to see the solution
Key Concept: Identify which operations the function applies to the element type, then make those the requirements on the template type parameter.
- Identify operations used on elements:
<,>,+=,-=. The typeTmust support all four. - Write the template:
#include <iostream>
using namespace std;
template <typename T>
void alignArray(T* array, int size, T barrier) {
for (int i = 0; i < size; i++) {
if (array[i] < barrier) array[i] += 2;
else if (array[i] > barrier) array[i] -= 2;
}
}- Declare a class satisfying requirements:
class Score {
int val;
public:
Score(int v = 0) : val(v) { }
bool operator<(const Score& s) const { return val < s.val; }
bool operator>(const Score& s) const { return val > s.val; }
Score& operator+=(int n) { val += n; return *this; }
Score& operator-=(int n) { val -= n; return *this; }
int value() const { return val; }
};- Demonstrate with the class:
int main() {
// Demo with int
int intArr[] = {1, 5, 10, 3, 7};
alignArray(intArr, 5, 5);
for (int x : intArr) cout << x << " "; // 3 5 8 5 5
cout << endl;
// Demo with Score
Score scores[] = {Score(1), Score(8), Score(5), Score(3)};
alignArray(scores, 4, Score(5));
for (auto& s : scores) cout << s.value() << " "; // 3 6 5 5
cout << endl;
return 0;
}Answer: The template parameterizes both the element type and the barrier type as T. Any type supporting <, >, +=, -= satisfies the requirements.
3.5. Stack Class Template with Non-type Size Parameter (Lecture 5, Example 3)
Implement a generic Stack class template parameterized by both the element type T and the maximum size N. Demonstrate creating stacks of different types and sizes.
Click to see the solution
Key Concept: Non-type template parameters allow compile-time configuration of class dimensions. Stack<int,10> and Stack<int,50> are different types.
#include <iostream>
#include <stdexcept>
using namespace std;
template <typename T, int N>
class Stack {
int top;
T S[N]; // Array size N is a compile-time constant
public:
Stack() : top(-1) { }
void push(const T& V) {
if (top >= N - 1) throw overflow_error("Stack is full");
S[++top] = V;
}
T pop() {
if (top < 0) throw underflow_error("Stack is empty");
return S[top--];
}
T peek() const {
if (top < 0) throw underflow_error("Stack is empty");
return S[top];
}
bool isEmpty() const { return top < 0; }
bool isFull() const { return top >= N - 1; }
};
int main() {
Stack<int, 10> intStack;
intStack.push(1);
intStack.push(2);
intStack.push(3);
cout << intStack.pop() << endl; // 3 (LIFO order)
cout << intStack.pop() << endl; // 2
Stack<string, 5> strStack;
strStack.push("hello");
strStack.push("world");
cout << strStack.pop() << endl; // "world"
// Stack<int, 10> and Stack<int, 50> are DIFFERENT types:
// Stack<int, 50> bigStack = intStack; // COMPILE ERROR
return 0;
}- Template declaration:
template <typename T, int N>— two parameters: element type and capacity. - Array:
T S[N]— N is evaluated at compile time; valid as an array size. - LIFO semantics:
pushincrementstopbefore writing;popreads attopthen decrements. - Bounds checking: Throws standard exceptions on overflow/underflow.
Answer: Stack<int,10> and Stack<string,5> are independent types generated from the same template. Output: 3, 2, world.
3.6. spaceOf: Explicit Function Template Instantiation (Lecture 5, Example 4)
Write a function template spaceOf<T>() that calculates how many 32-bit words (each 4 bytes) are needed to store a value of type T. Demonstrate explicit instantiation for several types.
Click to see the solution
Key Concept: When a function template has no arguments from which the compiler can deduce T, you must provide the type explicitly using <T> at the call site.
#include <iostream>
using namespace std;
template <typename T>
int spaceOf() {
int bytes = sizeof(T);
// Number of 32-bit words: ceiling division by 4
return bytes / 4 + (bytes % 4 > 0 ? 1 : 0);
}
class MyData {
double x, y, z; // 3 doubles = 24 bytes
int flag; // 4 bytes
// Total: 28 bytes = 7 × 4-byte words
};
int main() {
// Cannot call spaceOf() without explicit type — no arguments to deduce from
cout << spaceOf<int>() << endl; // 4 bytes → 1 word
cout << spaceOf<double>() << endl; // 8 bytes → 2 words
cout << spaceOf<char>() << endl; // 1 byte → 1 word (ceiling)
cout << spaceOf<MyData>() << endl; // 28+ bytes → 7+ words (depends on padding)
// The compiler optimizes: sizeof(int)=4 is a compile-time constant,
// so spaceOf<int>() is likely inlined to the constant 1.
return 0;
}- No deducible argument:
spaceOf()takes no arguments, so the compiler cannot inferT. - Explicit syntax:
spaceOf<int>()— the<int>tells the compiler to instantiate withT = int. - Compile-time optimization: Since
sizeof(T)is a compile-time constant, the entire function can be optimized away, leaving just the constant result. - Ceiling division formula:
bytes/4 + (bytes%4 > 0 ? 1 : 0)computes \(\lceil \text{bytes}/4 \rceil\).
Answer: spaceOf<int>() = 1, spaceOf<double>() = 2, spaceOf<char>() = 1.
3.7. RANGE Template: Complete Implementation (Tutorial 5, Example 1)
Write a complete implementation of the RANGE template providing:
- Constructor(s) and destructor
- Arithmetic and relational operators (
+=,-=,+,-,==,!=,<,>) - Increment and decrement operators
- Conversion function
RANGE → long - A checking and exception-handling mechanism
Show two realistic examples of using the RANGE template.
Click to see the solution
Key Concept: Non-type template parameters make the range boundaries part of the type, not the value. This means RANGE<1,12> and RANGE<1,31> are incompatible types — the compiler will flag any mix-up.
#include <iostream>
#include <stdexcept>
using namespace std;
template <int L, int R>
class RANGE {
int value;
void check() const {
if (value < L || value > R)
throw out_of_range("RANGE value " + to_string(value)
+ " out of [" + to_string(L) + "," + to_string(R) + "]");
}
public:
// Constructor from int
explicit RANGE(int v) : value(v) { check(); }
// Copy constructor
RANGE(const RANGE& r) : value(r.value) { }
// Destructor (trivial)
~RANGE() = default;
// --- Assignment ---
RANGE& operator=(const RANGE& r) { value = r.value; return *this; }
RANGE& operator=(int v) { value = v; check(); return *this; }
// --- Compound arithmetic ---
RANGE& operator+=(int n) { value += n; check(); return *this; }
RANGE& operator-=(int n) { value -= n; check(); return *this; }
// --- Binary arithmetic (return plain int for flexibility) ---
int operator+(int n) const { return value + n; }
int operator-(int n) const { return value - n; }
int operator+(const RANGE& r) const { return value + r.value; }
int operator-(const RANGE& r) const { return value - r.value; }
// --- Increment / Decrement ---
RANGE& operator++() { value++; check(); return *this; } // pre-increment
RANGE operator++(int) { RANGE tmp(*this); ++(*this); return tmp; } // post-increment
RANGE& operator--() { value--; check(); return *this; } // pre-decrement
RANGE operator--(int) { RANGE tmp(*this); --(*this); return tmp; } // post-decrement
// --- Relational ---
bool operator==(const RANGE& r) const { return value == r.value; }
bool operator!=(const RANGE& r) const { return value != r.value; }
bool operator< (const RANGE& r) const { return value < r.value; }
bool operator> (const RANGE& r) const { return value > r.value; }
// --- Conversion to long ---
operator long() const { return (long)value; }
};
// ---- Practical Examples ----
using DayOfMonth = RANGE<1, 31>;
using MonthOfYear = RANGE<1, 12>;
int main() {
// Example 1: Calendar date arithmetic
DayOfMonth day(15);
MonthOfYear month(6);
cout << "Day: " << (long)day << endl; // 15
cout << "Month: " << (long)month << endl; // 6
++day;
cout << "Next day: " << (long)day << endl; // 16
try {
day = 32; // out of range!
} catch (const out_of_range& e) {
cout << "Error: " << e.what() << endl;
}
// day = month; // COMPILE ERROR: incompatible types — safety guaranteed!
// Example 2: Traffic light phase (0=Red, 1=Yellow, 2=Green)
using Phase = RANGE<0, 2>;
Phase light(0);
for (int i = 0; i < 4; i++) {
cout << "Light phase: " << (long)light << endl;
try { ++light; } catch (...) { light = Phase(0); } // wrap around on overflow
}
return 0;
}- Template parameters as type identity:
DayOfMonth(alias forRANGE<1,31>) andMonthOfYear(RANGE<1,12>) are different types — mixing them is a compile error. check()called on every mutation: constructor,operator=,operator+=,operator++, etc.- Conversion to
long: Allows printing and using the value in arithmetic expressions without explicit casts. - Post-increment: Requires saving a copy before incrementing, returning the old value.
Answer: See complete implementation above. The key insight is that boundary enforcement happens at compile time (type system) and at runtime (exception).
3.8. ARRAY Template with RANGE-based Boundaries (Tutorial 5, Example 2)
Design and implement an ARRAY template where the valid index range is specified as template parameters (using ideas from the RANGE template). The array should support arbitrary index boundaries (not necessarily starting at 0). Implement an indexing operator with bounds checking. Demonstrate with a practical example.
Click to see the solution
Key Concept: Just like RANGE makes the value boundaries part of the type, ARRAY can make the index boundaries part of the type. ARRAY<int,1,12> is an array of 12 integers indexed from 1 to 12 — a “1-based array”.
#include <iostream>
#include <stdexcept>
#include <string>
using namespace std;
// ARRAY<T, Low, High>: an array of type T with indices in [Low, High]
template <typename T, int Low, int High>
class ARRAY {
static_assert(Low <= High, "ARRAY: Low must be <= High");
static const int SIZE = High - Low + 1;
T data[SIZE];
void checkIndex(int idx) const {
if (idx < Low || idx > High)
throw out_of_range("ARRAY index " + to_string(idx)
+ " out of [" + to_string(Low) + "," + to_string(High) + "]");
}
public:
ARRAY() = default;
// Non-const indexing (allows modification)
T& operator[](int idx) {
checkIndex(idx);
return data[idx - Low]; // Shift: external index → internal 0-based index
}
// Const indexing (for read-only access)
const T& operator[](int idx) const {
checkIndex(idx);
return data[idx - Low];
}
int low() const { return Low; }
int high() const { return High; }
int size() const { return SIZE; }
};
int main() {
// 1-based array of month names (indices 1..12)
ARRAY<string, 1, 12> monthNames;
monthNames[1] = "January";
monthNames[2] = "February";
monthNames[3] = "March";
// ... (fill remaining)
monthNames[12] = "December";
cout << monthNames[1] << endl; // "January"
cout << monthNames[12] << endl; // "December"
try {
monthNames[0]; // Index 0 is out of range [1, 12]
} catch (const out_of_range& e) {
cout << "Error: " << e.what() << endl;
}
// 2D array using ARRAY of ARRAYs:
ARRAY<ARRAY<int, 1, 3>, 1, 3> matrix;
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
matrix[i][j] = i * 10 + j;
cout << matrix[2][3] << endl; // 23
return 0;
}- Index shifting: The internal storage is 0-based (
data[0..SIZE-1]). The public interface uses[Low..High]. The translation isdata[idx - Low]. static_assert: A compile-time assertion that catches invalid template arguments (e.g.,ARRAY<int, 10, 5>would fail to compile with a clear message).- Two
operator[]overloads: One for mutable access (returnsT&), one for read-only access onconstarrays (returnsconst T&). - 2D array: Nesting
ARRAYinsideARRAYgives a 2D structure with full bounds checking on both indices.
Answer: The index shift data[idx - Low] is the key trick. Any index range can be used, and out-of-bounds access is detected at runtime.
3.9. Generic Smart Pointer: RAII Implementation (Tutorial 5, Example 3)
Implement a simple generic smart pointer class template smart_pointer<T> that:
- Takes ownership of a raw pointer passed to its constructor
- Automatically deletes the object when the smart pointer goes out of scope
- Supports
->and*operators for pointer-like usage - Add operators that make it look as similar to a raw pointer as possible
- Write a test showing the advantage over raw pointers
Click to see the solution
Key Concept: The RAII pattern guarantees that the destructor is called when the object leaves scope — even if an exception is thrown. This is the foundation of all smart pointers.
#include <iostream>
#include <stdexcept>
using namespace std;
template <typename T>
class smart_pointer {
T* obj;
public:
// Constructor: acquire ownership
explicit smart_pointer(T* o = nullptr) : obj(o) { }
// Destructor: release ownership (RAII core)
~smart_pointer() {
delete obj;
cout << "[smart_pointer: object deleted]" << endl;
}
// Prevent copying (like unique_ptr)
smart_pointer(const smart_pointer&) = delete;
smart_pointer& operator=(const smart_pointer&) = delete;
// Pointer-like operators
T* operator->() { return obj; }
T& operator*() { return *obj; }
// Conversion to raw pointer (read-only)
T* get() const { return obj; }
// Boolean check: is the pointer non-null?
explicit operator bool() const { return obj != nullptr; }
// Comparison with nullptr
bool operator==(nullptr_t) const { return obj == nullptr; }
bool operator!=(nullptr_t) const { return obj != nullptr; }
};
// A sample class to manage
class Resource {
int id;
public:
Resource(int i) : id(i) { cout << "Resource " << id << " created\n"; }
~Resource() { cout << "Resource " << id << " destroyed\n"; }
void use() { cout << "Using Resource " << id << "\n"; }
};
void demonstrateAdvantage() {
// With raw pointers: must remember to delete!
// Resource* raw = new Resource(99);
// raw->use();
// if (someCondition) return; // MEMORY LEAK if we forget delete here
// delete raw;
// With smart_pointer: automatic cleanup, even on early return
smart_pointer<Resource> sp(new Resource(1));
sp->use(); // Works like a raw pointer
(*sp).use(); // Also works
if (sp) {
cout << "Pointer is valid\n";
}
cout << "Leaving scope...\n";
} // smart_pointer destructor called automatically here — no delete needed!
int main() {
demonstrateAdvantage();
cout << "After scope exit\n";
return 0;
}Output:
Resource 1 created
Using Resource 1
Using Resource 1
Pointer is valid
Leaving scope...
[smart_pointer: object deleted]
Resource 1 destroyed
After scope exit
- RAII:
~smart_pointer()callsdelete obj— guaranteed to run when the smart pointer goes out of scope or an exception unwinds the stack. - Deleted copy: Prevents two
smart_pointerinstances from owning the same object (would cause double-delete). operator->: Returns the raw pointer, enablingsp->use()syntax.operator bool(): Allowsif (sp)checks.- Advantage over raw: Even if
demonstrateAdvantagereturned early (e.g., exception), the destructor would still run — no memory leak.
Answer: The RAII destructor is the critical mechanism. See output above for the automatic cleanup sequence.